# pwr_cmdr_bmp.pl
#
# April 2010, James Redus   
#
# This script is released under the terms of the MIT License:
# --------------------------------------------------------------
## Permission is hereby granted, free of charge, to any person
## obtaining a copy of this software and associated documentation
## files (the "Software"), to deal in the Software without
## restriction, including without limitation the rights to use,
## copy, modify, merge, publish, distribute, sublicense, and/or sell
## copies of the Software, and to permit persons to whom the
## Software is furnished to do so, subject to the following
## conditions:
##
## The above copyright notice and this permission notice shall be
## included in all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
## OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
## HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
## WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
## OTHER DEALINGS IN THE SOFTWARE.
# --------------------------------------------------------------
#
# Functions for writing Bitmap files from power commander
# hashes.  The PC hashes are output from read_pvm_file() or
# read_djm_file() functions.
#
# See comments in map_2_bmp() sub for input arguments.
#
# Example of creating bmp from PC hash:
# ------------------------
# #!/usr/bin/perl
#
# require "getopts.pl";
# require 'dumpvar.pl';
# require 'pwr_cmdr.pl';
# require 'pwr_cmdr_bmp.pl';
# 
# $mapfile="mymap.djm";
# $bmp_file="mymap.bmp";
#
# %h=read_djm_file($mapfile,0); 
#
# # Create a 300x500 interpolated (smoothed) bmp with color_peak=25:
# map_2_bmp(\%h,$bmp_file,300,500,1,25,0);
#
#
# ------------------------
#
#
#
#
#
###########################################################
###########################################################
###########################################################
###########################################################
sub min($$)
{
    ($a,$b)=@_;

    if($a<$b){
        return($a);
    }else{
        return($b);
    }
}
###########################################################
sub max($$)
{
    ($a,$b)=@_;

    if($a>$b){
        return($a);
    }else{
        return($b);
    }
}
###########################################################
#
# This sub interpolates a pixels fuel map value. I.e., 
# find the fractional value for a pixel depending on how far it is
# from the center of the cell. E.g., if a pixel is halfway between 
# fuel map values 3 and 4, it's value would be 3.5.
#
# The interpolation algorithm used here is simplistic, and may not be all that 
# accurate (I dunno how DynoJet makes adjustments to fuel pulses, etc,
# so I just took a wild guess). In the sub below, a pixel coord is passed,
# along with other info about this size of the bitmap, etc, and the x,y
# coords of the four map-cell-centers that 'enclose' that pixel is determined.
# After determining the values of the cell centers above and below a
# given pixel, an above and below horiz interpolated value is calculated,
# and then the verical interpolated value is calculated for the pixel.
# The final value (a floating point number) is returned to the calling func,
# and a color is applied to the output bmp based on that value and the 
# color range being used.
#
#        C...............I........C
#                        .
#                        .
#                        .
#                        .
#                        .
#                        p
#                        .
#                        .
#        C...............I........C
#
#
#
#
sub cell_val
{
    my ($row,$col,$rows,$cols,$rpm_count,$tp_count,$interpolate,@map) = @_;
    my (%x,%y,$m,$n,%tp,%rpm,%cellval,$tp,$rpm);

    $col_width=$cols/$tp_count;
    $row_height=$rows/$rpm_count;

    if (! $interpolate) {
        $tp= int($col/$col_width);
        $tp=min($tp,$tp_count - 1);

        $rpm= int($row/$row_height);
        $rpm=min($rpm,$rpm_count - 1);

        return($map[ $rpm*$tp_count + $tp ]);
    }    

    %x=%y=%tp=%rpm=%cellval=(); 

    $half_col_width=$cols/(2 * $tp_count);
    $n=min(int($col/$half_col_width), 2 * $tp_count -1);

    $half_row_height=$rows/(2 * $rpm_count);
    $m=min(int($row/$half_row_height), 2 * $rpm_count -1);

    # See which 'quadrant' we're in and find center pixels ...
    # I need the center point (x,y coord) of the 4 cells that the given pixel falls into,
    # so I can interpolate the colors.
    if( ($n % 2) == 0) {  # even number of half-column_widths
        # B_L = bottom left, etc
        $x{B_L}=($n - 1) * $half_col_width;
        $x{B_R}=($n + 1) * $half_col_width;
    }else{  # odd number of half-column widths 
        $x{B_L}=($n) * $half_col_width;
        $x{B_R}=($n + 2) * $half_col_width;
    }
    $x{B_L}=max($x{B_L},$half_col_width);
    $x{B_L}=min($x{B_L}, (2 * $tp_count -1) * $half_col_width);
    $x{T_L}=$x{B_L};
    $x{B_R}=max($x{B_R},$half_col_width);
    $x{B_R}=min($x{B_R}, (2 * $tp_count -1) * $half_col_width);
    $x{T_R}=$x{B_R};

    $tp{T_L}=int($x{T_L}/$col_width);
    $tp{T_R}=int($x{T_R}/$col_width);
    $tp{B_L}=int($x{B_L}/$col_width);
    $tp{B_R}=int($x{B_R}/$col_width);

    if( ($m % 2) == 0) {  # even number of half-row widths
        $y{B_L}=($m + 1) * $half_row_height;

        $y{T_L}=($m - 1) * $half_row_height;

    }else{  # odd number of half-row widths 
        $y{B_L}=($m + 2) * $half_row_height;
        $y{T_L}=($m) * $half_row_height;
    }
    $y{B_L}=max($y{B_L},$half_row_height);
    $y{B_L}=min($y{B_L}, (2 * $rpm_count -1) * $half_row_height);
    $y{B_R}=$y{B_L};
    $y{T_L}=max($y{T_L},$half_row_height);
    $y{T_L}=min($y{T_L}, (2 * $rpm_count -1) * $half_row_height);
    $y{T_R}=$y{T_L};

    $rpm{T_L}=int($y{T_L}/$row_height);
    $rpm{T_R}=int($y{T_R}/$row_height);
    $rpm{B_L}=int($y{B_L}/$row_height);
    $rpm{B_R}=int($y{B_R}/$row_height);

    $cellval{T_L}=$map[$rpm{T_L}*$tp_count + $tp{T_L}];
    $cellval{T_R}=$map[$rpm{T_R}*$tp_count + $tp{T_R}];
    $cellval{B_L}=$map[$rpm{B_L}*$tp_count + $tp{B_L}];
    $cellval{B_R}=$map[$rpm{B_R}*$tp_count + $tp{B_R}];

    # Interpolate ...
    # Top.
    if ($x{T_L} == $x{T_R}) {
        $cellval{T}=$cellval{T_L};
    }else{
        $denom=abs($x{T_R}-$x{T_L});
        $xdiff=abs($col-$x{T_L});
        $celldiff=$cellval{T_R}-$cellval{T_L};
        $cellval{T}=($xdiff/$denom)*$celldiff + $cellval{T_L};
    }

    # Bottom.
    if ($x{B_L} == $x{B_R}) {
        $cellval{B}=$cellval{B_L};
    }else{
        $denom=abs($x{B_R}-$x{B_L});
        $xdiff=abs($col-$x{B_L});
        $celldiff=$cellval{B_R}-$cellval{B_L};
        $cellval{B}=($xdiff/$denom)*$celldiff + $cellval{B_L};
    }

    # Final.
    if ($y{T_L} == $y{B_L}) {
        $cellval{FINAL}=$cellval{T};
    }else{
        $denom=abs($y{B_L}-$y{T_L});
        $ydiff=abs($row-$y{T_L});
        $celldiff=$cellval{B}-$cellval{T};
        $cellval{FINAL}=($ydiff/$denom)*$celldiff + $cellval{T};
    }

    # printf("\t\t\tInterpolated (T/B/FINAL)=%.3f/%.3f/%.3f\n",$cellval{T},$cellval{B},$cellval{FINAL});

    return($cellval{FINAL});

}
###########################################################
# This sub creates a bitmap file based on data read from a
# dynojet file.
#
# Input params:
#   href = hash ref to hash returned by read_pvm_file or read_djm_file
#   file_name = output file name (to be created in this sub)
#   pixel_cols,pixel_rows = number of columns, rows in bitmap
#   interpolate = create interpolated bitmap if this is non-zero
#   color_peak = the fuel cell value that gets the maximum color. 
#                This should be something like 25.
#   status = pint a dot each second if this is non-zero
#
# Output: An 8-bit-per-pixel bitmap file. The 8-bit bitmap uses a 
# color palette to hold a range of colors, and each pixel's data 
# is just an index into the color palette. 
#
sub map_2_bmp
{
    my ($href,$file_name,$pixel_cols,$pixel_rows,$interpolate,$color_peak,$status) = @_;
    my ($rem,$file_size,$palette,$bmp_header,$dib_header,$bmp_data);
    my (%h,$tp_count,$rpm_count,@fuel_array,$rpm,$tp,%color);
    my ($cell_width,$cell_height,$t);
    
    open(OUT,">$file_name") || die "Unable to write $file_name ($!)\n";
    binmode(OUT);

    %color=();

    %h=%{$href};  # a hash containing fuel map info
    @fuel_array=@{$h{fuel_map}};
    $rpm_count=$h{fuel_rpm_count};

    if ($h{map_type} eq "PC3") {
        # print "PC3 Map\n"; 
        $tp_count=9;
    }elsif($h{map_type} eq "PC5" ) {
        # print "PC5 Map\n"; 
        $tp_count=10;
        # Autotune trim setting is applied by default
    }else{
        print "Unknown hash type - aborting!\n";
        exit(1);
    }

    # BMP header=14 bytes
    # DIB header=40 bytes
    # palette=length($palette) bytes 
    # bitmap data = rows x cols bytes (8-bit bmp)
   
    $rem = $pixel_cols % 4;
    $pixel_cols += 4-$rem if($rem !=0);  # make divisible by 4
    $rem = $pixel_rows % 4;
    $pixel_rows += 4-$rem if($rem !=0);  # make divisible by 4

    # --------------------------------
    # Fuel map colors (B,G,R): 
    #   Max Blue: 255,0,0
    #   Min Blue: 255,247,247
    #   Max Red : 0,0,255
    #   Min Red : 247,247,255
    #
    # Normally colors are specified RGB, but packed this way they are BGR (final byte ignored)
    # $palette=pack("CCCC",0,0,0,0); BLACK
    # $palette=pack("CCCC",255,0,0,0); # BLUE
    # $palette=pack("CCCC",0,255,0,0); # GREEN
    # $palette=pack("CCCC",0,0,255,0); # RED
    # $palette=pack("CCCC",255,255,255,0); # WHITE
    #
    #
    # If it were packed into a 32-bit [x86] integer, it would be RGB (with 1st byte ignored).
    # $palette=pack("I",0x00FF0000); RED
    # $palette=pack("I",0x0000FF00); GREEN
    # $palette=pack("I",0x000000FF); BLUE
    # $palette=pack("I",0x00000000); BLACK
    # $palette=pack("I",0x00FFFF00); YELLOW

    $palette="";
    # Blue : light->dark
    for($i=247;$i>0; $i-=2) {
        $palette .= pack("CCCC",255,$i,$i,0);
    }

    $color_count=length($palette)/4;
    # The RANGE of blue (min->max)
    $color{blue_min_idx}=0;
    $color{blue_max_idx}=$color_count - 1;
    $color{blue_color_count}=$color{blue_max_idx} - $color{blue_min_idx} + 1;

    # Red : light->dark
    for($i=247;$i>0; $i-=2) {
        $palette .= pack("CCCC",$i,$i,255,0);
    }
    $color_count=length($palette)/4;
    $color{red_min_idx}=$color{blue_max_idx}+1;
    $color{red_max_idx}=$color_count - 1;
    $color{red_color_count}=$color{red_max_idx}-$color{red_min_idx}+1;

    $palette .= pack("CCCC",255,255,255,0);  # white
    $color_count=length($palette)/4;
    $color{white}=$color_count-1;
    $color{0}=$color_count-1;

    # $palette .= pack("CCCC",255,255,255,0);  # white
    $palette .= pack("CCCC",0,0,0,0);  # black
    $color_count=length($palette)/4;
    $color{black}=$color_count-1;

    # dumpValue(\%color);  # DEBUG
    
    # $file_size=14 + 40 + length($palette) + $pixel_rows * $pixel_cols;
    $file_size=14 + 40 + length($palette) + $pixel_rows * $pixel_cols + 24;
    
    # print "fsize=$file_size\n";
    
    # "BM",fsize,0,bmp data offset:
    # $bmp_header=pack("aaIII",B,M,$file_size,0,14+40+length($palette));
    $bmp_header=pack("aaIII",B,M,$file_size,0,14+40+length($palette) + 24);

    #-----------------------------------------------------------------------------------------------------
    # NOTE: The original BMP file looked wierd in MSPaint,Firefox and other progs until I add this filler.
    # When I first noticed that Paint didn't like my original BMP, I compared a BMP 
    # output of this script, and a BMP output of IrfanView, and noticed that IrfanView
    # added an extra 24 chars, which I don't think is used for anything. So I added the extra 
    # 24 chars, and Firefox and Paint and other progs display the BMP correctly. 
    # Still haven't figured out why this is, but since it works
    # this way, I'll keep it! The filler chars can apparently be set to any value.
    #-----------------------------------------------------------------------------------------------------

    $filler="J" x 24;  
    
    # hdr_len(40),width,ht,color_planes(1),bits_per_pixel(8),compression(0),bmp_size,horiz/vert res (pix/meter),colors,imp. colors
    # Negative height flips bitmap 'upside down' (beginning of data is at top of bmp).
    # $dib_header=pack("IIISSIIIIII",40,$pixel_cols,(-1)*$pixel_rows,1,8,0,$pixel_rows*$pixel_cols,2835,2835,256,0);
    # $dib_header=pack("IIISSIIIIII",40,$pixel_cols,(-1)*$pixel_rows,1,8,0,$pixel_rows*$pixel_cols,2835,2835,256,0);
    # IE would not show the bmp if the neg height option was used, so switched pos and reversed data row order.
    $dib_header=pack("IIISSIIIIII",40,$pixel_cols,(+1)*$pixel_rows,1,8,0,$pixel_rows*$pixel_cols,2835,2835,256,0);
    
    # $cell_height=($pixel_cols/$rpm_count);
    # $cell_width=($pixel_rows/$tp_count);
    $cell_height=($pixel_rows/$rpm_count);
    $cell_width=($pixel_cols/$tp_count);
    $t=time();
    $bmp_data="";
    # for($row=1;$row<=$pixel_rows;$row++) {  # Reversed row order so IE would recognize bmps.
    for($row=$pixel_rows;$row>=1;$row--) {
        my $color_idx,$color_offset,$pos_val,$cell_val,$t1;
        $t1=time();
        # Print a dot each second.
        if ($t1 != $t) {
            print "." if ($status);
            $t=$t1;
        }

        for($col=1;$col<=$pixel_cols;$col++) {
            $cell_val=cell_val($row,$col,$pixel_rows,$pixel_cols,$rpm_count,$tp_count,$interpolate,@fuel_array);
            # NOTE TO SELF: MAY NEED TO GO OVER THIS SECTION (DUE TO INTERPOLATED COLORS).
            if ($cell_val>0) {
                if ($interpolate) {
                    $color_offset=($color{blue_color_count}/$color_peak) * ($cell_val);
                }else{
                    $color_offset=($color{blue_color_count}/$color_peak) * ($cell_val - 1);
                }
                $color_offset=int(min($color_offset,$color{blue_color_count}-1));
                $color_idx=$color{blue_min_idx} + $color_offset;

            }elsif ($cell_val<0) {
                $pos_val=(-1) * $cell_val;
                if ($interpolate) {
                    $color_offset=($color{red_color_count}/$color_peak) * ($pos_val );
                }else{
                    $color_offset=($color{red_color_count}/$color_peak) * ($pos_val - 1);
                }
                # print "DEBUG1: [$cell_val],color_offset=$color_offset\n";
                $color_offset=int(min($color_offset,$color{red_color_count}-1));
                $color_idx=$color{red_min_idx} + $color_offset;
            }else{
                $color_idx=$color{white};
            }
            # print "color_idx=$color_idx\n";

            $bmp_data .= pack("C",$color_idx);
        }
    }
    # print "\n" if ($status);
    
    # print OUT $bmp_header,$dib_header,$palette,$bmp_data;
    print OUT $bmp_header,$dib_header,$palette,$filler,$bmp_data;

    close(OUT);


}
###########################################################
1;
